package net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.Random;

import util.Logger;

/**
 * Represents a connection to another computer. Through this, messages can be
 * sent and received.
 * 
 * @author Jason
 * 
 */

public abstract class Connection implements Runnable, Protocol {

	private static final Random rand = new Random();

	protected Socket tcpSocket;
	protected String ip;
	protected int tcpPort, udpInbound, udpOutbound;
	protected Thread tcpThread, udpPacketListener;
	protected InputStream is;
	protected OutputStream os;
	protected final LinkedList<TCPConnectionListener> tcpListeners = new LinkedList<TCPConnectionListener>();
	protected final LinkedList<UDPConnectionListener> udpListeners = new LinkedList<UDPConnectionListener>();

	public InetAddress getAddress() {
		return tcpSocket.getInetAddress();
	}

	public int getUDPOutbound() {
		return udpOutbound;
	}

	public void setUDPOutbound(int port) {
		udpOutbound = port;
	}

	public void addTCPConnectionListener(TCPConnectionListener cl) {
		synchronized (tcpListeners) {
			tcpListeners.add(cl);
		}
	}

	public void removeTCPConnectionListener(TCPConnectionListener cl) {
		synchronized (tcpListeners) {
			tcpListeners.remove(cl);
		}
	}

	public void addUDPConnectionListener(UDPConnectionListener cl) {
		synchronized (udpListeners) {
			udpListeners.add(cl);
		}
	}

	public void removeUDPConnectionListener(UDPConnectionListener cl) {
		synchronized (tcpListeners) {
			tcpListeners.remove(cl);
		}
	}

	/**
	 * Sends the message using TCP.
	 * 
	 * @param message
	 *            The message sent to the other computer.
	 */
	public synchronized void send(String message) {
		sendTCP(message.getBytes());
	}

	/**
	 * Sends the specified bytes over TCP.
	 */
	public synchronized void sendTCP(byte[] bytes) {
		try {
			os.write(bytes);
			os.write('\n');
			os.flush();
		} catch (IOException e) {
			System.err.println("Error sending message.");
		}
	}

	public void sendUDP(byte[] bytes) {
		sendUDP(bytes, 0, bytes.length);
	}

	public abstract void sendUDP(byte[] bytes, int offset, int len);

	@Override
	public void run() {
		ByteBuffer bb = new ByteBuffer();
		while (tcpThread != null) {
			byte[] msg = readMessage(bb);
			if (msg == null) {
				exit();
				break;
			} else if (msg.length > 0) {
				if (msg[0] == DELIM_CHAR) {
					// this message was meant to be intercepted right here.
					intercept(msg);
				} else {
					synchronized (tcpListeners) {
						for (TCPConnectionListener listener : tcpListeners) {
							listener.tcpMessageReceived(msg, this);
						}
					}
				}
			}
			bb.reset();
		}
		Logger.log(Logger.FINEST, "Connection ended.");
	}

	protected abstract void intercept(byte[] msg);

	protected int getPossiblePort() {
		return 20000 + rand.nextInt(10000);
	}

	public void udpReceived(byte[] bytes) {
		synchronized (udpListeners) {
			for (UDPConnectionListener listener : udpListeners) {
				listener.udpMessageReceived(bytes, this);
			}
		}
	}

	private byte[] readMessage(ByteBuffer bb) {
		while (true) {
			try {
				int i = is.read();
				if (i == -1)
					return null;
				else if (i == '\n')
					break;
				bb.add(i);
			} catch (Exception e) {
				return null;
			}
		}
		return bb.toByteArray();
	}

	protected synchronized void open() throws IOException {
		is = tcpSocket.getInputStream();
		os = tcpSocket.getOutputStream();
	}

	public String getIP() {
		return ip;
	}

	public int getTCPPort() {
		return tcpPort;
	}

	public synchronized void exit() {
		tcpThread = null;
		udpPacketListener = null;
		try {
			if (is != null) {
				synchronized (is) {
					is.close();
					is = null;
				}
			}
			if (os != null) {
				os.close();
				os = null;
			}
			if (tcpSocket != null) {
				tcpSocket.close();
				tcpSocket = null;
			}
		} catch (IOException e) {
			Logger.log(e);
		}
		for (TCPConnectionListener listener : tcpListeners) {
			listener.tcpConnectionBroken(this);
		}
		tcpListeners.clear();
	}

	@Override
	public String toString() {
		return getIP() + ":" + tcpPort;
	}

}
